#!/usr/bin/env python
#Usage: python ftpclient.py -h
#Uses T rather than self
#TODO: Continueing broken downloads. Recursive search. Gtk gui. UDP sockets. Hash checks.
#Raul Jimenez
"""Ftp client writen in python to support regex searches and downloading\nfolders. It is seperated into comandline menu thread and downloader thread."""
import ftplib, re, threading, os, optparse, sys

parser=optparse.OptionParser()
#defaults
parser.set_defaults(host='192.168.3.1')
parser.set_defaults(port=3000)
parser.set_defaults(user='imperator')
parser.set_defaults(passwd='koutei')
parser.set_defaults(dir='/home/imperator/anime/')
parser.set_defaults(host_dir='/storage/hdb/hdb6/')

parser.add_option("-H","--host",dest="host")
parser.add_option("-p","--port",type='int',dest="port")
parser.add_option("-u","--user",dest="user")
parser.add_option("-P","--passwd",dest="passwd")
parser.add_option("-d","--dir",dest="dir")
parser.add_option("-o","--host-dir",dest="host_dir") 

options=parser.parse_args()[0]

class MenuThread(threading.Thread):
	"""Thread to handle menu and input."""
	def __init__(T):
		global options, quit
		threading.Thread.__init__(T)
		T.ftp=ftplib.FTP()
		try: reconnect(T.ftp)
		except: 
			print "Incorrect User/Password. Exiting."
			sys.exit()
		try: change_dir(T.ftp,options.host_dir)
		except: print "Could not enter ",options.host_dir
		print "Entered ", T.ftp.pwd()
		T.current_list=create_list(T.ftp)
		T.actions={'ls':T.print_list,'dir':T.print_dir_list,'cd':T.change_dir,\
		'cwd':T.change_save,'pwd':T.print_path,'find':T.print_find,'get':T.queue_file,\
		'queue':T.print_queue,'quit':exit}
		T.pwd=T.ftp.pwd()
		T.save_dir=options.dir
	def print_menu(T):
		"""Print the menu."""
		print "Options: "
		(keys,values)=(T.actions.keys(),T.actions.values())
		for x in range(len(keys)): print"\t%s\t%s"%(keys[x],values[x].__doc__)
	def print_list(T, args=None):
		"""Print currentactive list."""
		for x in range(len(T.current_list)): print '%03i  %s:%s'%(x,T.current_list[x][1],T.current_list[x][0])
	def print_dir_list(T, args=None):
		"""Reload and print current directory list."""
		T.list_dir()
		T.print_list()
	def list_dir(T, args=None):
		"""Reload list from current directory."""
		T.current_list=create_list(T.ftp)
	def change_save(T, args):
		"""Change directory to save files to."""
		path=''
		for x in args[1:]:
			if path: path=path+' '+x
			else: path=x
		if not path[-1]=='/': path+='/'
		print 'Current save path:',path
		T.save_dir=path
	def change_dir(T, args):
		"""Change directory on host."""
		try: path=T.current_list[int(args[1])][0]
		except: 
			path = args[1]
			for x in args[2:]:
				path=' '+path
		T.ftp.cwd(path)
		T.pwd=T.ftp.pwd()
		T.print_dir_list()
		T.print_path()
	def queue_file(T, args):
		"""Queue a file for download. get num1[,num2, ...]"""
		global download_thread
		file_name=type=''
		for x in args[1:]:
			#try:
			(file_name,type)=(T.current_list[int(x)][0],T.current_list[int(x)][1])
			#except ValueError: ('^'+file_name+'$','',T.current_list)
			queue.append([file_name,type,T.pwd,T.save_dir])
		download_thread.inactive.set()
		download_thread.inactive.clear()
	def print_queue(T, args=None):
		"""Print download progress."""
		global queue, queue_pos, download_thread
		print "Current Standing:"
		(line,done)=('    %02i %s %s\n        %s %s\n        %s','*')
		for x in range(len(queue)):
			if x==queue_pos: 
				done='~'
				print download_thread.file_progress()
				print line%(x,'!',queue[x][0],queue[x][1],queue[x][2], queue[x][3])
			else: print line%(x,done,queue[x][0],queue[x][1],queue[x][2],queue[x][3])
	def print_path(T, args=None):
		"""Print current directory path."""
		print "Current host directory is:%s"%T.pwd
	def print_find(T, args):
		"""Regex search through currently active list. \n\t\tfind ['':all,'d':directories,'-':files] [regex]"""
		T.set_find(args)
		T.print_list(args)
	def set_find(T,args):
		"""Set current active list to those matching querry. if no query match for directory or file"""
		type=query=''
		idx=1
		if len(args[1])==1:
			type=args[1]
			idx=2
		for x in args[idx:]:
			if query: query=query+' '+x
			else: query=x
		T.current_list=find(query,type,T.current_list)
	def run(T):
		"""Function executed when menu thread is started."""
		global quit, download_thread
		while not quit:
			args=re.split(' ',raw_input('>>> '))
			if args[0] in T.actions.keys():
				try: T.actions[args[0]](args)
				except Exception, x:
					print 'Exception "%s" occured with command [%s] and options %s'%(str(x),str(args[0]), str(args[1:]))
					reconnect(T.ftp)
					change_dir(T.ftp,T.pwd)
			else: T.print_menu()
		download_thread.inactive.set()
		download_thread.inactive.set()
		print 'Exiting...'
class DownloadThread(threading.Thread):
	"""Thread to handle downloading files."""
	def __init__(T):
		threading.Thread.__init__(T)
		T.ftp=ftplib.FTP()
		try: reconnect(T.ftp)
		except: 
			print "Wrong User/Password exiting."
			sys.exit()
		T.file_size=T.current_size=0.0
		T.current_file=''
		T.grab={'-':T.get_file,'d':T.get_dir}
		T.inactive=threading.Event()
	def get_file(T,file_name,save):
		"""Download next file."""
		def get_block(block):
			"""Write block to file."""
			T.file.write(block)
			T.current_size+=len(block)
		cmd="RETR %s"
		T.current_file=file_name
		tmp=save+file_name
		T.file=open(tmp,'wb')
		T.file_size=T.ftp.size(file_name)
		T.ftp.retrbinary(cmd%file_name, get_block)
		T.file.close()
	def get_dir(T,directory,save):
		"""Recreate and download directory."""
		if not os.access(save+directory,os.F_OK): os.mkdir(save+directory)
		change_dir(T.ftp,directory)
		for x in create_list(T.ftp):
			#print [x[0],x[1],T.ftp.pwd(),save+directory+'/']
			T.get([x[0],x[1],T.ftp.pwd(),save+directory+'/'])
	def get(T,list):
		"""Grab file or dir."""
		change_dir(T.ftp,list[2])
		T.grab[list[1]](list[0],list[3])
	def file_progress(T):
		"""Returns progress of currently downloading file."""
		return "\t~! Current file:%s \n\t\tSize:%.02fmb Compleate: %.02f"%(T.current_file,(T.file_size/1048576),(T.current_size/T.file_size))
	def run(T):
		"""Function executed when download thread is started."""
		global quit,queue,queue_pos
		while True:
			if queue_pos==len(queue):
				if quit: break
				T.inactive.wait()
			else: 
				try: 
					T.get(queue[queue_pos])
					queue_pos+=1
				except Exception:
					reconnect(T.ftp)
					T.get(queue[queue_pos])
					queue_pos+=1
		print "Done."
quit=False		#weather to quit
queue=[]		#[0:filename,1:d/-,2:host path,3:save path]
queue_pos=0		#current queue position
download_thread=menu_thread=None
def change_dir(ftp, path):
	"""Change directory on host."""
	token=re.split('/',path)
	for x in range(len(token)):
		if x==0 and not token[0]: token[1]='/'+token[1]+'/'
		else: ftp.cwd(token[x])
def reconnect(ftp):
	"""Reconnect to host."""
	global options
	ftp.connect(host=options.host,port=options.port)
	ftp.login(user=options.user,passwd=options.passwd)
def create_list(ftp):
	"""Create list of files in current directory. [Name, d/-]"""
	def get_line(line):
		list.append([line[(line.find(re.split('[\s]*',line)[8])):],line[0]])
	list=[]
	ftp.retrlines('LIST', get_line)
	return list
def find(query,type,list):
	"""Match type, then match querry. Returns list of matches"""
	result=[]
	tmp=0
	for x in list:
		tmp+=1
		if (not type or type==x[1]) and re.search(query, x[0], re.I): result.append(x)
	return result
def exit(args=None):
	"""Quit the program after downloads finish."""
	global quit
	quit=True
def main():
	"""Main executuon thread. It starts other threads and does clean up at the end."""
	global download_thread, menu_thread
	if not options.user: options.user=raw_input("Enter user login: ")
	if not options.passwd: options.passwd=raw_input("Enter password for %s: "%options.user)
	if not options.host: options.host=raw_input("Enter host address: ")
	if not options.port: options.port=raw_input("Enter host port: ")
	if not options.dir: options.dir="/home/%s/"%options.user
	if not options.host_dir: options.host_dir="/home/%s/"%options.user
	#print options
	download_thread=DownloadThread()
	download_thread.start()
	menu_thread=MenuThread()
	menu_thread.start()
	download_thread.join()
	menu_thread.ftp.close()
	download_thread.ftp.close()

if __name__=="__main__":
	main()
